function isObject(obj) {
  // return obj !== null && typeof obj === 'object'
  return {}.toString.call(obj) === '[object Object]'
}

// 对于数据进行特殊处理
var arrayProto = Array.prototype
var arrayMethods = Object.create(arrayProto)
var original = arrayProto.push
arrayMethods.push = function (...args) {
  // 它本来就要干的活
  original.apply(this, args)
  // 通知视图更新
  console.log('视图更新')
}

// 响应式处理
function reactive(target, key, value) {
  // 递归一下,进行深层监听,它也是vue2它性能出问题的地方
  // 它只会在初始化时对已有属性进行劫持,新增属性需要额外手段才能被劫持
  // 现在只是对对象进行了劫持,数组是不是还没有处理
  // defineProperty它是可以监听数组的,但是它不能监听它的length长度,它只能监听数组已有的元素数据
  // [1,2,3] vue对于数组进行了特殊处理
  // 数组中有7个方法是可以改变源数据 push() pop() shift() unshift() splice() sort() reverse()
  observer(target[key])
  const dep = new Dep()
  Object.defineProperty(target, key, {
    get() {
      console.log('get')
      // 收集更新依赖
      Dep.target && dep.add(Dep.target)
      return value
    },
    set(newValue) {
      if (newValue !== value) {
        value = newValue
        console.log('set')
        dep.notify()
      }
      return true
    }
  })
}

// 进行数据的劫持处理
function observer(data) {
  // 如果不是对象,则不需要去劫持
  // if (!isObject(data)) throw new Error('你不是一个对象')
  // 循环对象
  if (isObject(data)) {
    for (let key in data) {
      reactive(data, key, data[key])
    }
  }
  // 数组进行特殊
  if (Array.isArray(data)) {
    // console.log('数组')
    // data.__proto__ = arrayMethods
    Object.setPrototypeOf(data, arrayMethods)
  }
}

// 在获取数据时,进行简易操作
function proxyData(vm, data) {
  for (let key in data) {
    Object.defineProperty(vm, key, {
      get() {
        return Reflect.get(data, key)
      },
      set(v) {
        if (v !== data[key]) {
          data[key] = v
        }
        return true
      }
    })
  }
}

class Vue {
  // 构造函数
  constructor(options) {
    if (!isObject(options)) throw new Error('必须要提供一个json对象')
    if (!options.el) throw new Error('el配置选项必须要有')
    this.$data = options.data
    // 如果你的el配置是一个字符串,我们就认为它是一个选择器自己去获取一下
    // 如果不是字符串,则为dom对象,就可以直接去使用
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el

    // 进行数据的响应式处理
    observer(this.$data)
    // 数据代理,便于在使用时,简易操作 app.$data.title ==> app.title
    proxyData(this, this.$data)

    // 模板的渲染
    new renderTemplate(this.$el, this.$data)
  }
}

// 渲染处理
class renderTemplate {
  constructor(root, data) {
    this.$data = data
    this.render(root)
  }

  /*
  nodeType 属性以数字形式返回指定节点的节点类型。
  如果节点是元素节点，则 nodeType 属性将返回 1。
  如果节点是属性节点，则 nodeType 属性将返回 2。
  如果节点是文本节点，则 nodeType 属性将返回 3。
  如果节点是注释节点，则 nodeType 属性将返回 8。
  */
  render(dom) {
    const nodes = dom.childNodes
    for (let [, node] of nodes.entries()) {
      // 现在就要来判断它是文件还是元素
      if (node.nodeType === 3) {
        this.renderText(node)
      } else if (node.nodeType === 1) {
        // 处理一下元素节点
        this.renderElement(node)

        // 如果它是一个元素节点就需要去查看是否有子元素,如果有则进入
        const childs = node.childNodes
        // 有子元素
        if (childs.length > 0) this.render(node)
      }
    }
  }

  // 处理文本节点
  renderText(node) {
    // {{ title }}
    if (/\{\{\s*(\w+)\s*\}\}/.test(node.textContent)) {
      const key = RegExp.$1
      node.textContent = this.$data[key]

      // 在此处放一下观察者,一会如果有数据更新,则在这里更新
      new Watcher(this.$data, key, val => {
        node.textContent = this.$data[key]
      })
    }
  }

  // 处理元素节点
  renderElement(node) {
    // attributes 获取当前元素它所有的属性
    const attrs = node.attributes
    // console.log([...attrs])
    Array.from(attrs).forEach(attr => {
      // 查找字符串是否以v-开头
      if (attr.name.startsWith('v-')) {
        if (attr.name === 'v-model') {
          // v-model 它就是一个专门给input表单项所用的,它是value和input事件的集合
          // v-model 语法糖
          node.value = this.$data[attr.value]
          node.addEventListener('input', e => {
            // 此处修改就会触发set
            this.$data[attr.value] = e.target.value.trim()
          })
        } else {
          node.innerHTML = this.$data[attr.value]
        }
        new Watcher(this.$data, attr.value, val => {
          node.innerHTML = this.$data[attr.value]
        })
      }
    })
  }
}

// 依赖收集者
class Dep {
  subs = new Set()
  add(watcher) {
    this.subs.add(watcher)
  }
  notify() {
    this.subs.forEach(watcher => watcher.updater())
  }
}

// 观察者
class Watcher {
  constructor(data, key, callback) {
    Dep.target = this
    this.$data = data
    this.$key = key
    this.$updater = callback
    // 获取了一下数据,触发get
    data[key]
    Dep.target = null
  }
  updater() {
    this.$updater(this.$data[this.$key])
  }
}
